package org.gienah.testing.junit;

import static org.gienah.testing.junit.ConfigurationException.ERROR_CODE_INVALID_CONTEXT;
import static org.gienah.testing.junit.ConfigurationException.ERROR_CODE_INVALID_CONTEXT_LOADER;
import static org.gienah.testing.junit.ConfigurationException.ERROR_CODE_MISSING_CONFIGURATION;
import static org.gienah.testing.junit.ConfigurationException.ERROR_CODE_MISSING_CONFIGURATION_LOCATIONS;
import static org.gienah.testing.junit.ConfigurationException.ERROR_CODE_PROVIDER_CYCLIC_REFERENCE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gienah.testing.xml.DependentDefinition;
import org.gienah.testing.xml.TestDefinition;
import org.junit.runner.Runner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ObjectUtils;

/**
 * @author Santiago L. Valdarrama
 * @since E0.2
 */
public final class CacheProvider {

	private static Map<String, ConfigurableApplicationContext> contextMap;
	private static Map<Class<?>, ConfigurableApplicationContext> testContextMap;
	private static Map<Class<?>, DependentDefinition> dependentDefinitionMap;

	static {
		CacheProvider.contextMap = new HashMap<String, ConfigurableApplicationContext>();
		CacheProvider.testContextMap = new HashMap<Class<?>, ConfigurableApplicationContext>();
		CacheProvider.dependentDefinitionMap = new HashMap<Class<?>, DependentDefinition>();
	}

	private CacheProvider() {
	}

	public static ConfigurableApplicationContext getContext(final Class<?> testClass)
			throws ConfigurationException {

		ConfigurableApplicationContext context = CacheProvider.testContextMap.get(testClass);

		if (context == null) {
			Configuration annotation = testClass.getAnnotation(Configuration.class);

			if (annotation == null) {
				throw new ConfigurationException(ERROR_CODE_MISSING_CONFIGURATION);
			}

			String[] locations = retrieveLocationsFrom(annotation, new ArrayList<Class<?>>());

			try {
				context = registerTestContext(testClass, annotation.loader().newInstance(), locations);
			}
			catch (InstantiationException e) {
				throw new ConfigurationException(ERROR_CODE_INVALID_CONTEXT_LOADER);
			}
			catch (IllegalAccessException e) {
				throw new ConfigurationException(ERROR_CODE_INVALID_CONTEXT_LOADER);
			}
		}

		return context;
	}

	public static TestDefinition getTestDefinition(final Class<?> testClass)
			throws ConfigurationException {

		TestDefinition testDefinition = (TestDefinition) CacheProvider.dependentDefinitionMap.get(testClass);
		if (testDefinition == null) {
			ConfigurableApplicationContext context = CacheProvider.getContext(testClass);
			if (context.getBeansOfType(TestDefinition.class).containsKey(testClass.getName())) {
				testDefinition = (TestDefinition) context.getBean(testClass.getName());
				CacheProvider.dependentDefinitionMap.put(testClass, testDefinition);
			}
		}

		return testDefinition;
	}

	public static DependentDefinition getRunnerDefinition(final Class<? extends Runner> runnerClass, final Class<?> testClass)
			throws ConfigurationException {

		DependentDefinition runnerDefinition = CacheProvider.dependentDefinitionMap.get(runnerClass);
		if (runnerDefinition == null) {
			ConfigurableApplicationContext context = CacheProvider.getContext(testClass);
			if (context.getBeansOfType(DependentDefinition.class).containsKey(runnerClass.getName())) {
				runnerDefinition = (DependentDefinition) context.getBean(runnerClass.getName());
				CacheProvider.dependentDefinitionMap.put(runnerClass, runnerDefinition);
			}
		}

		return runnerDefinition;
	}

	public static ConfigurableApplicationContext registerTestContext(final Class<?> testClass, final IContextLoader contextLoader, final String... locations)
			throws ConfigurationException {

		String contextKey = toContextKey(locations);
		ConfigurableApplicationContext context = CacheProvider.contextMap.get(contextKey);

		if (context == null) {
			context = contextLoader.loadContext(locations);

			if (context == null) {
				throw new ConfigurationException(ERROR_CODE_INVALID_CONTEXT);
			}

			context.registerShutdownHook();
			CacheProvider.contextMap.put(contextKey, context);
		}

		CacheProvider.testContextMap.put(testClass, context);

		return context;
	}

	public static ConfigurableApplicationContext register(final Class<?> testClass, final String... locations)
			throws ConfigurationException {

		return registerTestContext(testClass, new ContextLoader(), locations);
	}

	public static void unregister(final Class<?> testClass) {
		CacheProvider.dependentDefinitionMap.remove(testClass);
		CacheProvider.testContextMap.remove(testClass);
	}

	private static String[] retrieveLocationsFrom(final Configuration annotation, final List<Class<?>> providers)
			throws ConfigurationException {

		if (annotation == null) {
			throw new ConfigurationException(ERROR_CODE_MISSING_CONFIGURATION_LOCATIONS);
		}

		if (annotation.locations().length > 0) {
			return annotation.locations();
		}

		Class<?> provider = annotation.provider();

		if (providers.contains(provider)) {
			throw new ConfigurationException(ERROR_CODE_PROVIDER_CYCLIC_REFERENCE);
		}

		providers.add(provider);

		return retrieveLocationsFrom(provider.getAnnotation(Configuration.class), providers);
	}

	private static String toContextKey(final String[] locations) {
		return ObjectUtils.nullSafeToString(locations);
	}

}
